package clasificador.naivebayes;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import clasificador.ClasificadorEntrenado;
import datos.Atributo;
import datos.AtributoFinito;
import datos.Dato;
import datos.IBuilder;
import datos.MuestraClasificada;

/* Aparte de Clase, el resto de atributos tambi�n han de tener un n�mero finito de posibilidades para que funcione, pero realmente da igual. */
public class ClasificadorNaiveBayes<Clase> extends ClasificadorEntrenado<Clase> {
	private Map<Clase,Double> probAPriori;
	private Map<Clase,Map<Atributo<?>, Distribucion<?>>> probsCondicionadas;
	private Atributo<Clase> clase;
        private Integer parametroLaplace;
        private Boolean ignorarDegenerados;
	
	private static Double unoEntreRaizDeDosPi = 1.0/Math.sqrt(2.0*Math.PI);

        public ClasificadorNaiveBayes(MuestraClasificada<Clase> datosEntrenamiento, Map<String,Object> parametros)
        {
            super(datosEntrenamiento,parametros);
        }
        
        @Override
        protected void inicializarParametros(Map<String,Object> parametros)
        {         
            Object pLaplace = parametros.get("parametroLaplace");            
            if(pLaplace instanceof Integer)
            {                
                this.parametroLaplace = (Integer)pLaplace;                
            } else {
                this.parametroLaplace = 0;
            }
            
            Object pDegenerados = parametros.get("ignorarDegenerados");
            if(pDegenerados instanceof Boolean)
            {
                this.ignorarDegenerados = (Boolean)pDegenerados;
            } else {
                this.ignorarDegenerados = true;
            }
        }
        
    private static void trace(Object s) {
           // System.out.println(s);
    }
    
    private static interface Distribucion<T> {
        public double probabilidad(T t);
    }

    private class DistribucionGaussiana implements Distribucion<Double> {
        private double media, varianza;
        private Double singleValue = null;        
        
        private DistribucionGaussiana(Double media, Double varianza) {
            this.media = media;
            this.varianza = varianza;    
            
            /* Si la varianza es 0, es una distribucion discreta con un soporte de un solo elemento. Es una gaussiana degenerada */
            if (this.varianza == 0.0) {
                this.singleValue = media;
            }
        }
        public double probabilidad(Double t) {
            if (singleValue == null) {
        	return unoEntreRaizDeDosPi 
                * (1.0/Math.sqrt(varianza))*(Math.pow(Math.E, -1 * (t - media)*(t - media)/(2*varianza)));
            } else {
                if(ClasificadorNaiveBayes.this.ignorarDegenerados)
                {
                    return 1.0;
                } else {
                    return (t == this.singleValue?1.0:0.0);
                }
            }
        }
    }

    private class DistribucionDiscreta<T> implements Distribucion<T> {
        private Map<T, Double> frecuencias;
        private DistribucionDiscreta(Map<T, Double> frecuencias) {
            this.frecuencias = frecuencias;
        }
        public double probabilidad(T t) {            
            Double value = frecuencias.get(t);
            
            return (value != null)?value:0.0;
        }
    }

    /* Devuelve la probabilidad de que el atributo "attr" tenga el valor de dicho atributo en el dato "dato", dada la clase de dicho dato. */
    private <T> double likelihood(Dato dato, Atributo<T> attr, Clase k) {    	
    	Distribucion<T> distribucion = (Distribucion<T>)probsCondicionadas.get(k).get(attr);        
        T a = dato.getAtributo(attr);
        trace("P(" + attr.getNombre() + " = " + a + "|" + clase.getNombre() + " = " + k + ") = " + distribucion.probabilidad(a)); 
        if(distribucion == null) return 0.0;        
        return distribucion.probabilidad(a);
    }
	
    private Set<Clase> getPosiblesClases()
    {
   		return probAPriori.keySet();
    }
    
    private Double priori(Clase clase)
    {
    	return probAPriori.get(clase);
    }    
    
	public ClasificadorNaiveBayes(MuestraClasificada<Clase> datosEntrenamiento) {
		super(datosEntrenamiento);	
	}

	private class IBuilderDistribucionDiscreta<T> implements IBuilder<T,DistribucionDiscreta<T>>
	{
		private Map<T,Integer> frecuenciasTotales;
		private Integer total;
        private Set<T> rango;
                private Integer parametroLaplace;

        private boolean isFinite() {
            return this.rango != null;
        }
        
        public IBuilderDistribucionDiscreta(Set<T> rango, Integer parametroLaplace) {
            super();
            this.rango = rango;
            this.parametroLaplace = parametroLaplace;                        
        }
        public IBuilderDistribucionDiscreta(Integer parametroLaplace)
        {
            this(null,parametroLaplace);
        }
        
        public IBuilderDistribucionDiscreta() {
            this(null,0);
        }


		@Override
		public void add(T c) {
			Integer cur = frecuenciasTotales.get(c);
			
			if(cur == null)
			{
				cur = 0;
			}
			
			frecuenciasTotales.put(c,cur+1);
			total++;
		}

		@Override
		public DistribucionDiscreta<T> finish() {
			Map<T,Double> frecuencias = new HashMap<T,Double>(frecuenciasTotales.size());                        
                          
                        /* Corrección de Laplace */
                        trace(rango);
                        
                        if(this.rango != null && parametroLaplace != 0)
                        {
                            for(T c: this.rango)
                            {
                                trace(parametroLaplace);
                                if(frecuenciasTotales.get(c) == null || frecuenciasTotales.get(c) == 0)
                                {
                                    frecuenciasTotales.put(c,parametroLaplace);                            
                                    total += parametroLaplace;
                                }                            
                            }
                        }
                        
			Double dTotal = new Double(total);			
                        
			for(T c: frecuenciasTotales.keySet())
			{
                                frecuencias.put(c, new Double(frecuenciasTotales.get(c)) /  dTotal);
			}
			
			return new DistribucionDiscreta<T>(frecuencias);
		}

		@Override
		public void reset() {
			frecuenciasTotales = new HashMap<T,Integer>();
			total = 0;
		}
	}
	
	private class IBuilderDistribucionContinua implements IBuilder<Double,DistribucionGaussiana>
	{
		private Double media;
		private Double sumaCuadrados;
		private Double cuenta;   
                
		/* C�lculo incremental de la media y la varianza */
		/* Referencia: http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Weighted_incremental_algorithm */
		@Override		
		public void add(Double c) {
			cuenta++;
			Double delta = c - media;
			media = media + delta/cuenta;
			sumaCuadrados = sumaCuadrados + delta * (c - media); 
                }

		@Override
		public DistribucionGaussiana finish() {
			/* Utilizamos la cuasivarianza muestral, que se asume mejor en general, aunque poca importancia d� */
			Double varianza = sumaCuadrados / (cuenta - 1);
			
			trace(media);
			trace(varianza);                        
                        
			return new DistribucionGaussiana(media,varianza);
		}

		@Override
		public void reset() {
			media = 0.0;
			sumaCuadrados = 0.0;
			cuenta = 0.0;
		}
		
	}
	
	@Override
	protected void entrenar(MuestraClasificada<Clase> datosEntrenamiento) {
        trace("Training");
		this.clase = datosEntrenamiento.getClase();		
		this.probAPriori = new HashMap<Clase,Double>();		
		this.probsCondicionadas = new HashMap<Clase,Map<Atributo<?>,Distribucion<?>>>();
		
		Map<Clase,Map<Atributo<?>,IBuilder<?,? extends Distribucion<?>>>> constructores = 
            new HashMap<Clase,Map<Atributo<?>,IBuilder<?,? extends Distribucion<?>>>>();
		Map<Clase,Integer> cuentaPriori = new HashMap<Clase,Integer>();
		Integer total = 0;		
				
		for(Dato dato: datosEntrenamiento.getDatos())
		{
			for(Atributo<?> atributo: dato.getAtributos())
			{
				if(!atributo.equals(clase))
				{					
					this.addDatoEntrenamiento(constructores, cuentaPriori, dato, atributo);
				}
			}
		    total++;
		}	
                
                trace(total);
		
		for(Clase clase: cuentaPriori.keySet())
		{
			probAPriori.put(clase, new Double(cuentaPriori.get(clase)) / total);
			
			probsCondicionadas.put(clase, new HashMap<Atributo<?>, Distribucion<?>>());
			
			for(Atributo<?> atributo: constructores.get(clase).keySet())
			{
                Distribucion<?> d = constructores.get(clase).get(atributo).finish();
				probsCondicionadas.get(clase).put(atributo, d);
			}
		}                
	}	
	
	private void addDatoEntrenamiento(Map<Clase,Map<Atributo<?>,IBuilder<?,? extends Distribucion<?>>>> constructores,
            Map<Clase,Integer> cuentaPriori, Dato dato, Atributo<?> atributo)
	{
                /* Construcci�n de distribuciones */
		Map<Atributo<?>,IBuilder<?,? extends Distribucion<?>>> mapaPorAtributo = 
            constructores.get(dato.getAtributo(clase));
		
		if(mapaPorAtributo == null)
		{
			mapaPorAtributo = 
                new HashMap<Atributo<?>,IBuilder<?,? extends Distribucion<?>>>(dato.getAtributos().size());

			constructores.put(dato.getAtributo(clase),mapaPorAtributo);
		}
		
		IBuilder<Object,? extends Distribucion<Object>> constructor = 
            (IBuilder<Object,? extends Distribucion<Object>>)mapaPorAtributo.get(atributo);
		
		if(constructor == null)
		{
            /* Typesafe a tope, oiga */
            if (atributo instanceof AtributoFinito) {
                AtributoFinito<Object> atributoFinito = (AtributoFinito<Object>)atributo;
                constructor = new IBuilderDistribucionDiscreta<Object>(atributoFinito.getRango(),parametroLaplace);
                constructor.reset();
            } else if (atributo.getTipo().equals(Double.class)) {
                constructor = (IBuilder)new IBuilderDistribucionContinua();
                constructor.reset();
            } else {
                constructor = new IBuilderDistribucionDiscreta<Object>(parametroLaplace);
                constructor.reset();
            }
			mapaPorAtributo.put(atributo,constructor);
		}           
                
                
		
		constructor.add(dato.getAtributo(atributo));
		
		/* Probabilidades a priori */
		Integer cuenta = cuentaPriori.get(dato.getAtributo(clase));
		if(cuenta == null)
		{
			cuenta = 0;
		}
		
		cuenta++;
		
		cuentaPriori.put(dato.getAtributo(clase),cuenta);
	}
        
	@Override
	public Clase clasificar(Dato d) {
		Clase claseMax = null;
		Double maxFitness = 0.0;

		for(Clase posibleClase: getPosiblesClases())
		{
			Double fitness = priori(posibleClase);
            trace("FOR CLASS " + posibleClase);
			
			/* Nos sentimos especialmente orgullosos de que esto haya salido natural.
			 * 
			 * Si un atributo no est� en el dato, no se tiene en cuenta para calcular la probabilidad (lo cual, dado que se utiliza
			 * la regla del producto, no supone ning�n problema). Por otro lado, si un atributo no estaba en la muestra de entrenamiento,
			 * tambi�n se ignora, lo cual tiene todo el sentido del mundo, pues no nos aporta informaci�n por no tener datos en la muestra.
			 * 
			 * El resultado es que la definici�n del dato da igual, se utiliza toda la informaci�n de que se dispone para clasificar, 
			 * y la clasificaci�n nunca falla.
			 */
			for(Atributo<?> atributo: d.getAtributos())
			{	
                /* TODO this is probably not the best way to do this */
                if (atributo != this.clase) { 
                    Double probCondicionada = likelihood(d, atributo, posibleClase);

                    if(probCondicionada != null)
                    {
                        fitness *= probCondicionada;
                    }
                }
			}
			
			if(fitness >= maxFitness)
			{
				claseMax = posibleClase;
				maxFitness = fitness;
			}
		}	
		
		return claseMax;
	}
}

